// Copyright (c) 2019 Presto Labs Pte. Ltd.
// Author: jhkim

#pragma once

#include <memory>
#include <string>

#include <fmt/format.h>
#include <google/protobuf/util/json_util.h>
#include <pybind11/functional.h>
#include <pybind11/numpy.h>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>

#include "coin2/exchange/base/feed/update.h"
#include "coin2/exchange/feed.pb.h"
#include "coin2/feed/mgr/system.h"

namespace py = pybind11;

namespace coin2::exchange::driver {
namespace impl {

using coin2::exchange::base::feed::FeedUpdate;

std::string ProtoToJson(const google::protobuf::Message& message) {
  google::protobuf::util::JsonPrintOptions options;
  options.add_whitespace = false;
  options.always_print_primitive_fields = false;
  options.preserve_proto_field_names = true;

  std::string json_string;
  google::protobuf::util::MessageToJsonString(message, &json_string, options);
  return json_string;
}

struct PyBookLevel {
  double price = 0;
  double qty = 0;
  int64_t count = 0;

  py::dict as_dict() const {
    using namespace pybind11::literals;
    return py::dict("price"_a = price, "qty"_a = qty, "count"_a = count);
  }
};  // struct PyBookLevel

template <class BookIterator>
py::array_t<double> levels_as_array(int num_level, BookIterator iter, BookIterator end) {
  py::array_t<double> ret({num_level, 2});
  for (int i = 0; i < num_level; ++i) {
    if (iter != end) {
      *ret.mutable_data(i, 0) = iter->first;
      *ret.mutable_data(i, 1) = iter->second;
      ++iter;
    } else {
      *ret.mutable_data(i, 0) = 0;
      *ret.mutable_data(i, 1) = 0;
    }
  }
  return ret;
}

template <class BookIterator>
py::list levels_as_list(int num_level, BookIterator iter, BookIterator end) {
  py::list ret;
  for (int i = 0; i < num_level; ++i) {
    if (iter == end) {
      break;
    }
    ret.append(py::make_tuple(iter->first, iter->second));
    ++iter;
  }
  return ret;
}

class FeedUpdateCopy : public FeedUpdate {
 public:
  FeedUpdateCopy(const FeedUpdate& feed_update, size_t max_book_level)
      : FeedUpdate(feed_update.system_name(), feed_update.mea()) {
    topic_id_ = feed_update.topic_id();
    sequence_ = feed_update.sequence();
    timestamp_ = feed_update.timestamp();

    feed_type_ = feed_update.feed_type();
    native_symbol_ = feed_update.native_symbol();

    // hope it's permanent.. please..
    product_ = &feed_update.product();

    if (feed_update.is_book()) {
      bookown_.reset(new BookBuilder(*feed_update.GetBookBuilder(), max_book_level));
      SetBook(bookown_.get());
    }
    if (feed_update.is_trade()) {
      tradeown_.reset(new Trade(feed_update.trade()));
      SetTrade(tradeown_.get());
    }
  }

 private:
  std::unique_ptr<BookBuilder> bookown_;
  std::unique_ptr<Trade> tradeown_;
};

class PyBookView {
 public:
  explicit PyBookView(const FeedUpdate* upd) : upd_(upd), timestamp_(upd->timestamp()) {}

  explicit PyBookView(std::shared_ptr<FeedUpdate> upd_if_owned)
      : upd_if_owned_(upd_if_owned),
        upd_(upd_if_owned.get()),
        timestamp_(upd_if_owned->timestamp()) {
    CHECK(upd_);
  }

  ~PyBookView() = default;

  auto exchange() const -> int { return static_cast<int>(upd_->exchange()); }
  auto exchange_str() const -> std::string { return coin::proto::ExchangeType_Name(exchange()); }
  auto market() const -> int { return static_cast<int>(upd_->market()); }
  auto market_str() const -> std::string { return coin::proto::MarketType_Name(market()); }
  std::string mea() const { return upd_->mea().String(); }
  const std::string& relative_norm() const { return upd_->product().relative_norm(); }
  const std::string& absolute_norm() const { return upd_->product().absolute_norm(); }
  const std::string& feed_native_symbol() const { return upd_->product().feed_native_symbol(); }

  int64_t timestamp() const { return timestamp_; }

  void set_timestamp(int64_t timestamp_given) { timestamp_ = timestamp_given; }

  bool has_ask() const { return upd_->book().Ask0().has_value(); }

  bool has_bid() const { return upd_->book().Bid0().has_value(); }

  size_t depth_ask() const { return upd_->book().AskSize(); }

  size_t depth_bid() const { return upd_->book().BidSize(); }

  std::optional<PyBookLevel> ask0() const {
    std::optional<PyBookLevel> ret;
    if (upd_->book().Ask0().has_value()) {
      ret = PyBookLevel{upd_->book().Ask0().value().price, upd_->book().Ask0().value().qty, 0};
    }
    return ret;
  }

  std::optional<PyBookLevel> bid0() const {
    std::optional<PyBookLevel> ret;
    if (upd_->book().Bid0().has_value()) {
      ret = PyBookLevel{upd_->book().Bid0().value().price, upd_->book().Bid0().value().qty, 0};
    }
    return ret;
  }

  py::array_t<double> get_ask_array(size_t num_level) {
    return levels_as_array(num_level, upd_->book().AskBegin(), upd_->book().AskEnd());
  }

  py::array_t<double> get_bid_array(size_t num_level) {
    return levels_as_array(num_level, upd_->book().BidBegin(), upd_->book().BidEnd());
  }

  auto get_ask_list(size_t num_level) {
    return levels_as_list(num_level, upd_->book().AskBegin(), upd_->book().AskEnd());
  }

  auto get_bid_list(size_t num_level) {
    return levels_as_list(num_level, upd_->book().BidBegin(), upd_->book().BidEnd());
  }

  std::shared_ptr<PyBookView> TakeCopy(size_t max_book_level) const {
    std::shared_ptr<FeedUpdate> update(new FeedUpdateCopy(*upd_, max_book_level));
    return std::shared_ptr<PyBookView>(new PyBookView(update));
  }

  py::dict as_dict() {
    using namespace coin2::exchange::feed;
    using namespace pybind11::literals;
    return py::dict(
        "type"_a = FeedType_Name(upd_->feed_type()),
        "timestamp"_a = timestamp(),
        "asks"_a = get_ask_list(5),
        "bids"_a = get_bid_list(5),
        "market"_a = market_str(),
        "exchange"_a = exchange_str(),
        "mea"_a = mea(),
        "relative_norm"_a = relative_norm(),
        "absolute_norm"_a = absolute_norm());
  }

 private:
  std::shared_ptr<FeedUpdate> upd_if_owned_;
  const FeedUpdate* upd_;
  int64_t timestamp_;
};  // class PyBookView

class PyBookViewPickleable {
 public:
  explicit PyBookViewPickleable(
      const std::string& type_str,
      const std::string& mea_str,
      const std::string& market_str,
      const std::string& exchange_str,
      const std::string& relative_norm,
      const std::string& absolute_norm,
      const std::string& native_symbol,
      int64_t timestamp,
      py::list asks,
      py::list bids)
      : type_str_(type_str),
        mea_str_(mea_str),
        market_str_(market_str),
        exchange_str_(exchange_str),
        relative_norm_(relative_norm),
        absolute_norm_(absolute_norm),
        native_symbol_(native_symbol),
        timestamp_(timestamp),
        asks_(asks),
        bids_(bids) {}
  ~PyBookViewPickleable() = default;

  auto type_str() const -> const std::string& { return type_str_; }
  auto mea_str() const -> const std::string& { return mea_str_; }
  auto market_str() const -> const std::string& { return market_str_; }
  auto exchange_str() const -> const std::string& { return exchange_str_; }

  auto relative_norm() const -> const std::string& { return relative_norm_; }
  auto absolute_norm() const -> const std::string& { return absolute_norm_; }
  auto native_symbol() const -> const std::string& { return native_symbol_; }
  auto timestamp() const -> int64_t { return timestamp_; }

  bool has_ask() const { return ask0().has_value(); }
  bool has_bid() const { return bid0().has_value(); }

  auto depth_ask() const -> size_t { return py::len(asks_); }
  auto depth_bid() const -> size_t { return py::len(bids_); }

  auto ask0() const -> std::optional<PyBookLevel> {
    std::optional<PyBookLevel> ret;
    for (auto item : asks_) {
      py::tuple t = item.cast<py::tuple>();
      ret = PyBookLevel{t[0].cast<double>(), t[1].cast<double>(), 0};
      break;
    }
    return ret;
  }

  auto bid0() const -> std::optional<PyBookLevel> {
    std::optional<PyBookLevel> ret;
    for (auto item : bids_) {
      py::tuple t = item.cast<py::tuple>();
      ret = PyBookLevel{t[0].cast<double>(), t[1].cast<double>(), 0};
      break;
    }
    return ret;
  }

  auto get_ask_array(size_t num_level) const -> py::array_t<double> {
    // py::array_t<double> ret({num_level, 2});
    // auto ask_size = depth_ask();
    // auto num = std::min(num_level, ask_size);
    // for (auto item : asks_) {
    //   if (num <= 0) {
    //     break;
    //   }
    //   py::tuple t = item.cast<py::tuple>();
    //   *ret.mutable_data(i, 0) = t[0].cast<double>();
    //   *ret.mutable_data(i, 1) = t[1].cast<double>();
    //   --num;
    // }
    py::array_t<double> ret;
    return ret;
  }

  auto get_bid_array(size_t num_level) const -> py::array_t<double> {
    // py::array_t<double> ret({num_level, 2});
    // for (int i = 0; i < num_level; ++i) {
    //   if (i < bid_size) {
    //     py::tuple t = .cast<py::tuple>();
    //     *ret.mutable_data(i, 0) = iter->first;
    //     *ret.mutable_data(i, 1) = iter->second;
    //     ++iter;
    //   } else {
    //     *ret.mutable_data(i, 0) = 0;
    //     *ret.mutable_data(i, 1) = 0;
    //   }
    // }
    // auto bid_size = depth_bid();
    // auto num = std::min(num_level, bid_size);
    // for (auto item : bids_) {
    //   if (num <= 0) {
    //     break;
    //   }
    //   py::tuple t = item.cast<py::tuple>();
    //   *ret.mutable_data(i, 0) = t[0].cast<double>();
    //   *ret.mutable_data(i, 1) = t[1].cast<double>();
    //   --num;
    // }
    py::array_t<double> ret;
    return ret;
  }

  auto get_ask_list(size_t num_level) const -> py::list {
    py::list ret;
    auto num = std::min(num_level, depth_ask());
    for (auto item : asks_) {
      if (num <= 0) {
        break;
      }
      py::tuple t = item.cast<py::tuple>();
      ret.append(py::make_tuple(t[0].cast<double>(), t[1].cast<double>()));
      --num;
    }
    return ret;
  }

  auto get_bid_list(size_t num_level) const -> py::list {
    py::list ret;
    auto num = std::min(num_level, depth_bid());
    for (auto item : bids_) {
      if (num <= 0) {
        break;
      }
      py::tuple t = item.cast<py::tuple>();
      ret.append(py::make_tuple(t[0].cast<double>(), t[1].cast<double>()));
      --num;
    }
    return ret;
  }

  auto asks() const -> py::list { return asks_; }
  auto bids() const -> py::list { return bids_; }

  py::dict as_dict() {
    using namespace coin2::exchange::feed;
    using namespace pybind11::literals;
    return py::dict(
        "type"_a = type_str(),
        "mea"_a = mea_str(),
        "market"_a = market_str(),
        "exchange"_a = exchange_str(),
        "relative_norm"_a = relative_norm(),
        "absolute_norm"_a = absolute_norm(),
        "native_symbol"_a = native_symbol(),
        "timestamp"_a = timestamp(),
        "asks"_a = get_ask_list(5),
        "bids"_a = get_bid_list(5));
  }

 private:
  const std::string type_str_;
  const std::string mea_str_;
  const std::string market_str_;
  const std::string exchange_str_;
  const std::string relative_norm_;
  const std::string absolute_norm_;
  const std::string native_symbol_;
  int64_t timestamp_;
  py::list asks_;
  py::list bids_;

};  // class PyBookViewPickleable

class PyTradeView {
 public:
  explicit PyTradeView(const FeedUpdate* upd) : upd_(upd), timestamp_(upd->timestamp()) {}
  ~PyTradeView() = default;

  auto exchange() const -> int { return static_cast<int>(upd_->exchange()); }
  auto exchange_str() const -> std::string { return coin::proto::ExchangeType_Name(exchange()); }
  auto market() const -> int { return static_cast<int>(upd_->market()); }
  auto market_str() const -> std::string { return coin::proto::MarketType_Name(market()); }
  std::string mea() const { return upd_->mea().String(); }
  const std::string& relative_norm() const { return upd_->product().relative_norm(); }
  const std::string& absolute_norm() const { return upd_->product().absolute_norm(); }
  const std::string& feed_native_symbol() const { return upd_->product().feed_native_symbol(); }
  double price() const { return upd_->trade().price; }
  double qty() const { return upd_->trade().fill_qty; }
  int side() const { return upd_->trade().side; }
  bool has_more_trade() const { return false; }

  int64_t timestamp() const { return timestamp_; }

  int64_t exchange_publish_timestamp() const {
	  return upd_->trade().timestamp;
  }
  
  void set_timestamp(int64_t timestamp_given) { timestamp_ = timestamp_given; }

  explicit PyTradeView(std::shared_ptr<FeedUpdate> upd_if_owned)
      : upd_if_owned_(upd_if_owned),
        upd_(upd_if_owned.get()),
        timestamp_(upd_if_owned->timestamp()) {}

  std::shared_ptr<PyTradeView> TakeCopy() const {
    std::shared_ptr<FeedUpdate> update(new FeedUpdateCopy(*upd_, 0));
    return std::shared_ptr<PyTradeView>(new PyTradeView(update));
  }

  py::dict as_dict() const {
    using namespace coin2::exchange::feed;
    using namespace coin::proto;
    using namespace pybind11::literals;
    return py::dict(
        "type"_a = FeedType_Name(upd_->feed_type()),
        "timestamp"_a = timestamp(),
        "side"_a = TradeSide_Name(side()),
        "price"_a = price(),
        "qty"_a = qty(),
        "market"_a = market_str(),
        "exchange"_a = exchange_str(),
        "mea"_a = mea(),
        "relative_norm"_a = relative_norm(),
        "absolute_norm"_a = absolute_norm(),
        "native_symbol"_a = feed_native_symbol(),
        "has_more_trade"_a = has_more_trade());
  }

 private:
  std::shared_ptr<FeedUpdate> upd_if_owned_;
  const FeedUpdate* upd_;
  int64_t timestamp_;
};  // class PyTradeView

class PyTradeViewPickleable {
 public:
  explicit PyTradeViewPickleable(
      const std::string& type_str,
      const std::string& mea_str,
      const std::string& market_str,
      const std::string& exchange_str,
      const std::string& relative_norm,
      const std::string& absolute_norm,
      const std::string& native_symbol,
      int64_t timestamp,
      double trade_price,
      double trade_qty,
      int trade_side,
      bool has_more_trade)
      : type_str_(type_str),
        mea_str_(mea_str),
        market_str_(market_str),
        exchange_str_(exchange_str),
        relative_norm_(relative_norm),
        absolute_norm_(absolute_norm),
        native_symbol_(native_symbol),
        timestamp_(timestamp),
        trade_price_(trade_price),
        trade_qty_(trade_qty),
        trade_side_(trade_side),
        has_more_trade_(has_more_trade) {}
  ~PyTradeViewPickleable() = default;

  auto type_str() const -> const std::string& { return type_str_; }
  auto mea_str() const -> const std::string& { return mea_str_; }
  auto market_str() const -> const std::string& { return market_str_; }
  auto exchange_str() const -> const std::string& { return exchange_str_; }

  auto relative_norm() const -> const std::string& { return relative_norm_; }
  auto absolute_norm() const -> const std::string& { return absolute_norm_; }
  auto native_symbol() const -> const std::string& { return native_symbol_; }
  auto timestamp() const -> int64_t { return timestamp_; }

  auto price() const -> double { return trade_price_; }
  auto qty() const -> double { return trade_qty_; }
  auto side() const -> int { return trade_side_; }
  auto side_str() const -> std::string {
    return coin2::exchange::feed::TradeSide_Name(trade_side_);
  }
  bool has_more_trade() const { return has_more_trade_; }

  py::dict as_dict() const {
    using namespace pybind11::literals;
    return py::dict(
        "type"_a = type_str(),
        "mea"_a = mea_str(),
        "market"_a = market_str(),
        "exchange"_a = exchange_str(),
        "relative_norm"_a = relative_norm(),
        "absolute_norm"_a = absolute_norm(),
        "native_symbol"_a = native_symbol(),
        "timestamp"_a = timestamp(),
        "side"_a = side_str(),
        "price"_a = price(),
        "qty"_a = qty(),
        "has_more_trade"_a = has_more_trade());
  }

 private:
  const std::string type_str_;
  const std::string mea_str_;
  const std::string market_str_;
  const std::string exchange_str_;
  const std::string relative_norm_;
  const std::string absolute_norm_;
  const std::string native_symbol_;
  int64_t timestamp_;
  double trade_price_;
  double trade_qty_;
  int trade_side_;
  bool has_more_trade_;
};  // class PyTradeViewPickleable

}  // namespace impl

using impl::FeedUpdateCopy;
using impl::PyBookLevel;
using impl::PyBookView;
using impl::PyTradeView;

}  // namespace coin2::exchange::driver
